﻿/*
<File>
	<Copyright>Copyright © 2007, Outcoder. All rights reserved.</Copyright>
	<License see="//License.txt"/>
	<Owner Name="Daniel Vaughan" Email="danielvaughan@outcoder.com"/>
	<CreationDate>2007/11/19 20:00</CreationDate>
	<LastSubmissionDate>$Date: $</LastSubmissionDate>
	<Version>$Revision: $</Version>
</File>
*/

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Security;
using System.Threading;
using System.Web.Management;
using System.Web.Security;
using System.Xml;
using Outcoder.Logging.Configuration;
using Outcoder.Logging.LogEntries;

namespace Outcoder.Logging
{
	/// <summary>
	/// The main application log.
	/// Use this static class to write to a
	/// <see cref="ILogStrategy"/> defined in the current 
	/// <see cref="ILogStrategyProvider"/>.
	/// </summary>
	public static partial class Log
	{
//		static bool useAspNetMembership;
//		static bool partialTrust = true;
		static int logEntrySentTimeOut = 15000;

		static Log()
		{
			SkipFrameCount = 4;

			try
			{
				InitFromConfig();
			}
			catch (Exception ex)
			{
				if (InternalLog.FatalEnabled)
				{
					InternalLog.Fatal("Unable to init from config.", ex);
				}
			}
		}

		public static LogLevel InternalLogLevel
		{
			get
			{
				return InternalLog.LogLevel;
			}
			set
			{
				InternalLog.LogLevel = value;
			}
		}

		public static LogRepository LogRepository
		{
			get
			{
				return LogRepository.Instance;
			}
		}

		public static int SkipFrameCount { get; set; }

		static bool ValidateFilters(ILogStrategy logStrategy, LogEntryOrigin origin, IClientInfo entry)
		{
			ArgumentValidator.AssertNotNull(logStrategy, "logStrategy");
			ArgumentValidator.AssertNotNull(entry, "entry");

			var filters = LogRepository.Instance.GetFilters(logStrategy);
			
			/* If no filters have been defined then allow the entry. */
			if (filters == null)
			{
				return true;
			}

			foreach (IFilter filter in filters)
			{
				if (!filter.IsValid(origin, entry))
				{
					return false;
				}
			}
			return true;
		}

		internal static LogLevel GetLogLevel(IClientInfo info)
		{	
			LogLevel minumLevel = LogLevel.None;

			foreach (var logStrategy in LogRepository.Instance.LogStrategies)
			{
				var level = logStrategy.GetLogLevel(info);
				if (level < minumLevel)
				{
					minumLevel = level;
				}
			}

			return minumLevel;
		}

		internal static void LogMessage(IClientLogEntry logEntry)
		{
			/* TODO: use parallel foreach. */
			foreach (var logStrategy in LogRepository.Instance.LogStrategies)
			{
				if (IsEnabled(logStrategy, LogEntryOrigin.Remote, logEntry))
				{
					logStrategy.Write(logEntry);
				}
			}
		}

		/// <summary>
		/// Loads the providers.
		/// </summary>
		static void InitFromConfig()
		{
			InternalLog.Debug("Initializing from config."); /* TODO: Make localizable resource. */

			var configurationElement = ConfigurationManager.GetSection("Clog") as XmlElement;
			if (configurationElement != null)
			{
				var logLevelAttribute = configurationElement.Attributes["InternalLogLevel"];
				LogLevel logLevel = LogLevel.None;
				bool configurationInvalid = false;
				if (logLevelAttribute != null && !string.IsNullOrEmpty(logLevelAttribute.Value))
				{
					try
					{
						logLevel = (LogLevel)Enum.Parse(typeof(LogLevel), logLevelAttribute.Value);
					}
					catch (Exception ex)
					{
						configurationInvalid = true;
						InternalLog.Error("InternalLogLevel attribute is invalid.", ex); /* TODO: Make localizable resource. */
					}
				}
				InternalLog.LogLevel = logLevel;

				var skipFrameCountAttribute = configurationElement.Attributes["SkipFrameCount"];
				if (skipFrameCountAttribute != null && !string.IsNullOrEmpty(skipFrameCountAttribute.Value))
				{
					int frameCount;
					if (int.TryParse(skipFrameCountAttribute.Value, out frameCount))
					{
						SkipFrameCount = frameCount;
					}
					else
					{
						configurationInvalid = true;
						InternalLog.Error("SkipFrameCount attribute is invalid. Value should be an integer. Attribute value: " 
							+ skipFrameCountAttribute.Value);
					}
				}

				LogRepository.Instance.Load(configurationElement);
				InternalLog.Info(string.Format("Clog configuration loaded {0}.", 
					configurationInvalid ? "with errors" : "successfully")); /* TODO: Make localizable resource. */
			}
			else
			{
				InternalLog.Info("Clog config section not found."); /* TODO: Make localizable resource. */
			}
			

//			partialTrust = !IsPermissionGranted(new SecurityPermission(PermissionState.Unrestricted));
//			section = (ClientLoggingConfigSection)WebConfigurationManager.GetSection("ClientLogging");
//			useAspNetMembership = section.UseAspNetMembership && partialTrust;
		}

		static bool IsPermissionGranted(IPermission requestedPermission)
		{
			try
			{
				/* Try and get this permission. */
				requestedPermission.Demand();
				return true;
			}
			catch
			{
				return false;
			}
		}

		/// <summary>
		/// Determines whether logging to the specified origin is enabled.
		/// Checks whether the current provider is null 
		/// and if not, then whether all filters are validate.
		/// Each <see cref="IFilter"/> makes use of the origin
		/// parameter and decides whether it is, itself, valid.
		/// If any filters are not valid, then this method
		/// returns <code>false</code>.
		/// </summary>
		/// <param name="logStrategy">The log strategy to test for whether it is enabled.</param>
		/// <param name="origin">The origin.</param>
		/// <param name="info">Client information used during evaluation.</param>
		/// <returns>
		/// 	<c>true</c> if logging is enable; otherwise, <c>false</c>.
		/// </returns>
		public static bool IsEnabled(ILogStrategy logStrategy, LogEntryOrigin origin, IClientInfo info)
		{
			try
			{
				return IsEnabledAux(logStrategy, origin, info);
			}
			catch (Exception ex)
			{
				InternalLog.Error("Error determining enabled status of log.", ex);
			}
			return false;
		}

		public static bool IsEnabled(LogEntryOrigin origin, IClientInfo info)
		{
			foreach (var logStrategy in LogRepository.Instance.LogStrategies)
			{
				if (IsEnabledAux(logStrategy, origin, info))
				{
					/* If even one strategy is enabled, then logging is enabled. */
					return true;
				}
			}
			return false;
		}

		static bool IsEnabledAux(ILogStrategy logStrategy, LogEntryOrigin origin, IClientInfo info)
		{
			return ValidateFilters(logStrategy, origin, info);
		}

		static bool IsLevelEnabled(LogLevel logLevel)
		{
			try
			{
				return IsLevelEnabledAux(logLevel);
			}
			catch (Exception ex)
			{
				InternalLog.Error("IsLevelEnabledAux raised exception.", ex);
			}
			return false;
		}

		static bool IsLevelEnabledAux(LogLevel logLevel)
		{
			foreach (var logStrategy in LogRepository.Instance.LogStrategies)
			{
				LogLevel strategyLevel;
				try
				{
					strategyLevel = logStrategy.GetLogLevel(null);
				}
				catch (Exception ex)
				{
					InternalLog.Error("Problem evaluating LogStrategy LogLevel.", ex);
					continue;
				}

				if (strategyLevel < logLevel)
				{
					return true;
				}
			}
			return false;
		}

		static void WriteLogEntry(LogLevel logLevel, string message, Exception exception, IDictionary<string, object> properties)
		{
			try
			{
				WriteLogEntryAux(logLevel, message, exception, properties);
			}
			catch (Exception ex)
			{
				if (InternalLog.ErrorEnabled)
				{
					InternalLog.Error("Error writing log.", ex);
				}
			}			
		}

		static void WriteLogEntryAux(LogLevel logLevel, string message, Exception exception, IDictionary<string, object> properties)
		{
			var codeLocation = GetCallerLocation(SkipFrameCount);
			var entry = new ServerLogEntry(logLevel, message)
			                   	{
									CodeLocation = codeLocation,
			                   		LogName = codeLocation.ClassName,
			                   		MachineName = SafeEnvironmentValues.MachineName,
			                   		UserName = SafeEnvironmentValues.UserName,
									OccuredAt = DateTime.Now,
									Properties = properties
			                   	};

			InternalLog.Debug("Writing log entry:" + entry);

			PopulateMembershipInfo(entry);

			bool eventRaised = false;
			object eventRaisedLock = new object();

			foreach (var logStrategy in LogRepository.Instance.LogStrategies)
			{
				try
				{
					if (!IsEnabled(logStrategy, LogEntryOrigin.Local, entry))
					{
						continue;
					}
				}
				catch (Exception ex)
				{
					InternalLog.Error(string.Format("Log strategy {0} failed on call to IsEnabled.", logStrategy), ex);
					continue;
				}

				if (exception != null)
				{
					entry.Exception = exception;
				}

				ILogStrategy logStrategy1 = logStrategy;
				ThreadPool.QueueUserWorkItem(
					delegate 
						{ 
							try
							{
								logStrategy1.Write(entry);
								if (!eventRaised)
								{
									ThreadPool.QueueUserWorkItem(
										delegate
		                             		{
		                             			try
		                             			{
													Monitor.TryEnter(eventRaisedLock, logEntrySentTimeOut);
		                             				try
		                             				{
		                             					if (!eventRaised)
		                             					{
		                             						OnLogEntrySent(new LogEventArgs(entry));
		                             						eventRaised = true;
		                             					}
		                             				}
		                             				finally
		                             				{
		                             					Monitor.Exit(eventRaisedLock);
		                             				}
		                             			}
		                             			catch (Exception ex)
		                             			{
		                             				System.Diagnostics.Debug.WriteLine(ex);
		                             			}
											}); /* end ThreadPool.QueueUserWorkItem */
								} /* end if */
							}
							catch (Exception ex)
							{
								if (InternalLog.ErrorEnabled)
								{
									InternalLog.Error("Problem writing log to ILogStrategy " + logStrategy1.GetType(), ex);
								}
							}
						});
			}
		}

		static bool membershipEnabled;

		static void PopulateMembershipInfo(LogEntryData data)
		{
			if (/*useAspNetMembership && */membershipEnabled && string.IsNullOrEmpty(data.UserName))
			{	/* Populate the UserName property using the Membership provider. */
				MembershipUser user = null;
				string errorMessage = null;

				try
				{
					user = Membership.GetUser(true);
				}
				catch (Exception ex)
				{
					if (ex is SqlExecutionException 
						|| ex is SecurityException
						|| ex.InnerException is SqlExecutionException)
					{
						membershipEnabled = false;
						/* TODO: Make localizable resource. */
						errorMessage = string.Format("Unable to user ASP.NET Membership to get username. Will not try again. Exception: {0}", ex);
					}
					else
					{
						/* TODO: Make localizable resource. */
						errorMessage = string.Format("Unable to user ASP.NET Membership to get username. Exception: {0}", ex);
					}
					//System.Diagnostics.Debug.WriteLine(errorMessage, traceCategory);
				}
				if (!string.IsNullOrEmpty(errorMessage))
				{
					Info(errorMessage);
				}
				if (user != null)
				{
					data.UserName = user.UserName;
				}
			}
		}

		/* TODO: Make an extensibility point for GetCallerLocation. */
		/// <summary>
		/// Gets the caller location from the <see cref="StackTrace"/>.
		/// </summary>
		/// <returns>The code location that the call to log originated.</returns>
		static CodeLocation GetCallerLocation(int methodCallCount)
		{
			string className;
			string methodName;
			string fileName;
			int lineNumber;

			var stackTrace = new StackTrace(methodCallCount, true);
			var stackFrame = stackTrace.GetFrame(0);

			if (stackFrame != null)
			{
				className = stackFrame.GetMethod().ReflectedType.FullName;
				methodName = stackFrame.GetMethod().Name;
				fileName = stackFrame.GetFileName();
				lineNumber = stackFrame.GetFileLineNumber();
			}
			else
			{
				className = string.Empty;
				methodName = string.Empty;
				fileName = string.Empty;
				lineNumber = -1;
			}

			CodeLocation callerLocation = new CodeLocation
			{
				ClassName = className,
				MethodName = methodName,
				FileName = fileName,
				LineNumber = lineNumber
			};
			return callerLocation;
		}

		#region Write event
		static event EventHandler<LogEventArgs> write;

		/// <summary>
		/// Occurs when the active <see cref="ILogStrategy"/>
		/// is called upon to write a log entry.
		/// </summary>
		public static event EventHandler<LogEventArgs> Write
		{
			add
			{
				write += value;
			}
			remove
			{
				write -= value;
			}
		}

		static void OnLogEntrySent(LogEventArgs e)
		{
			if (write != null)
			{
				write(null, e);
			}
		}
		#endregion
	}
}
